/*
 * Copyright (c) XMEDIA. All rights reserved.
 */

#include <linux/bitops.h>
#include <linux/clk.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/reset.h>

#define PWM_CLOCK_ADDR_BASE 0x120101BC
#define PWM_CLOCK_3M 3000000
#define PWM_CLOCK_24M 24000000
#define PWM_CLOCK_50M 50000000

#define PWM_CFG0_ADDR(x) (((x)*0x20) + 0x0)
#define PWM_CFG1_ADDR(x) (((x)*0x20) + 0x4)
#define PWM_CFG2_ADDR(x) (((x)*0x20) + 0x8)
#define PWM_CTRL_ADDR(x) (((x)*0x20) + 0xC)

#define PWM_ENABLE_SHIFT 0
#define PWM_ENABLE_MASK  BIT(0)

#define PWM_POLARITY_SHIFT 1
#define PWM_POLARITY_MASK  BIT(1)

#define PWM_KEEP_SHIFT 2
#define PWM_KEEP_MASK  BIT(2)

#define PWM_PERIOD_MASK GENMASK(31, 0)
#define PWM_DUTY_MASK   GENMASK(31, 0)

struct xmedia_pwm_chip {
	struct pwm_chip chip;
	unsigned long clk;
	void __iomem *base;
};

struct xmedia_pwm_soc {
	u32 num_pwms;
};

static const struct xmedia_pwm_soc pwm_soc[1] = {
	{ .num_pwms = 4 },
};

static inline struct xmedia_pwm_chip *to_xmedia_pwm_chip(struct pwm_chip *chip)
{
	return container_of(chip, struct xmedia_pwm_chip, chip);
}

static void xmedia_pwm_set_bits(void __iomem *base, u32 offset,
							   u32 mask, u32 data)
{
	void __iomem *address = base + offset;
	u32 value;

	value = readl(address);
	value &= ~mask;
	value |= (data & mask);
	writel(value, address);
}

static void xmedia_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm)
{
	struct xmedia_pwm_chip *bsp_pwm_chip = to_xmedia_pwm_chip(chip);

	xmedia_pwm_set_bits(bsp_pwm_chip->base, PWM_CTRL_ADDR(pwm->hwpwm),
					  PWM_ENABLE_MASK, 0x1);
}

static void xmedia_pwm_disable(struct pwm_chip *chip, struct pwm_device *pwm)
{
	struct xmedia_pwm_chip *bsp_pwm_chip = to_xmedia_pwm_chip(chip);

	xmedia_pwm_set_bits(bsp_pwm_chip->base, PWM_CTRL_ADDR(pwm->hwpwm),
					  PWM_ENABLE_MASK, 0x0);
}

static void xmedia_pwm_config(struct pwm_chip *chip, struct pwm_device *pwm,
							 int duty_cycle_ns, int period_ns)
{
	struct xmedia_pwm_chip *bsp_pwm_chip = to_xmedia_pwm_chip(chip);
	u32 duty;
	u64 period, freq;

	freq = div_u64(bsp_pwm_chip->clk, 1000000);

	period = div_u64(freq * period_ns, 1000);
	duty = div_u64(period * duty_cycle_ns, period_ns);

	xmedia_pwm_set_bits(bsp_pwm_chip->base, PWM_CFG0_ADDR(pwm->hwpwm),
					  PWM_PERIOD_MASK, period);

	xmedia_pwm_set_bits(bsp_pwm_chip->base, PWM_CFG1_ADDR(pwm->hwpwm),
					  PWM_DUTY_MASK, duty);
}

static void xmedia_pwm_set_polarity(struct pwm_chip *chip,
								  struct pwm_device *pwm,
								  enum pwm_polarity polarity)
{
	struct xmedia_pwm_chip *bsp_pwm_chip = to_xmedia_pwm_chip(chip);

	if (polarity == PWM_POLARITY_INVERSED)
		xmedia_pwm_set_bits(bsp_pwm_chip->base, PWM_CTRL_ADDR(pwm->hwpwm),
						   PWM_POLARITY_MASK, (0x1 << PWM_POLARITY_SHIFT));
	else
		xmedia_pwm_set_bits(bsp_pwm_chip->base, PWM_CTRL_ADDR(pwm->hwpwm),
						   PWM_POLARITY_MASK, (0x0 << PWM_POLARITY_SHIFT));
}

static void xmedia_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
								struct pwm_state *state)
{
	struct xmedia_pwm_chip *bsp_pwm_chip = to_xmedia_pwm_chip(chip);
	void __iomem *base;
	u32 freq;
	u64 value;

	freq = div_u64(bsp_pwm_chip->clk, 1000000);
	base = bsp_pwm_chip->base;

	value = readl(base + PWM_CFG0_ADDR(pwm->hwpwm));
	state->period = div_u64(value * 1000, freq);

	value = readl(base + PWM_CFG1_ADDR(pwm->hwpwm));
	state->duty_cycle = div_u64(value * 1000, freq);

	value = readl(base + PWM_CTRL_ADDR(pwm->hwpwm));
	state->enabled = (PWM_ENABLE_MASK & value);
}

static int xmedia_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
			  const struct pwm_state *state)
{
	if (state->polarity != pwm->state.polarity) {
		xmedia_pwm_set_polarity(chip, pwm, state->polarity);
	}

	if (state->period != pwm->state.period ||
		state->duty_cycle != pwm->state.duty_cycle) {
		xmedia_pwm_config(chip, pwm, state->duty_cycle, state->period);
	}

	if (state->enabled != pwm->state.enabled) {
		if (state->enabled) {
			xmedia_pwm_enable(chip, pwm);
		} else {
			xmedia_pwm_disable(chip, pwm);
		}
	}

	return 0;
}

static void get_pwm_clk(unsigned long *clk)
{
	void *pwm_addr = (void *)ioremap(PWM_CLOCK_ADDR_BASE, 0x4);
	unsigned int val = readl(pwm_addr);
	val = (val >> 8) & 0x3;
	switch (val) {
	case 0:
		*clk = PWM_CLOCK_3M;
		break;
	case 1:
		*clk = PWM_CLOCK_50M;
		break;
	default:
		*clk = PWM_CLOCK_24M;
		break;
	}
	iounmap(pwm_addr);
}

static struct pwm_ops xmedia_pwm_ops = {
	.get_state = xmedia_pwm_get_state,
	.apply = xmedia_pwm_apply,

	.owner = THIS_MODULE,
};

static int xmedia_pwm_probe(struct platform_device *pdev)
{
	const struct xmedia_pwm_soc *soc =
		of_device_get_match_data(&pdev->dev);
	struct xmedia_pwm_chip *pwm_chip;
	struct resource *res;
	int ret;
	int i;

	pwm_chip = devm_kzalloc(&pdev->dev, sizeof(*pwm_chip), GFP_KERNEL);
	if (pwm_chip == NULL) {
		return -ENOMEM;
	}

	get_pwm_clk(&(pwm_chip->clk));
	if (!pwm_chip->clk) {
		dev_err(&pdev->dev, "getting clock failed with %lu\n", pwm_chip->clk);
		return -EINVAL;
	}

	pwm_chip->chip.ops = &xmedia_pwm_ops;
	pwm_chip->chip.dev = &pdev->dev;
	pwm_chip->chip.base = -1;
	pwm_chip->chip.npwm = soc->num_pwms;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	pwm_chip->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(pwm_chip->base)) {
		return PTR_ERR(pwm_chip->base);
	}

	ret = pwmchip_add(&pwm_chip->chip);
	if (ret < 0) {
		clk_disable_unprepare((struct clk *)&pwm_chip->clk);
		return ret;
	}

	for (i = 0; i < pwm_chip->chip.npwm; i++) {
		xmedia_pwm_set_bits(pwm_chip->base, PWM_CTRL_ADDR(i),
						   PWM_KEEP_MASK, (0x1 << PWM_KEEP_SHIFT));
	}

	platform_set_drvdata(pdev, pwm_chip);

	return 0;
}

static int xmedia_pwm_remove(struct platform_device *pdev)
{
	struct xmedia_pwm_chip *pwm_chip;

	pwm_chip = platform_get_drvdata(pdev);

	clk_disable_unprepare((struct clk *)&pwm_chip->clk);

	return pwmchip_remove(&pwm_chip->chip);
}

static const struct of_device_id xmedia_pwm_of_match[] = {
	{ .compatible = "xmedia,xmedia-pwm" },
	{ .compatible = "xmedia,pwm", .data = &pwm_soc[0] }, {}
};
MODULE_DEVICE_TABLE(of, xmedia_pwm_of_match);

static struct platform_driver xmedia_pwm_driver = {
	.driver = {
		.name = "xmedia-pwm",
		.of_match_table = xmedia_pwm_of_match,
	},
	.probe = xmedia_pwm_probe,
	.remove = xmedia_pwm_remove,
};
module_platform_driver(xmedia_pwm_driver);

MODULE_AUTHOR("Xmedia");
MODULE_DESCRIPTION("Xmedia SoCs PWM driver");
MODULE_LICENSE("GPL");
